ASP曾经非常流行,我上大学那会儿,图书馆书架上写Web编程的书籍里面讲ASP的占了大多数,给我一个错觉,ASP就是最牛逼的编程语言。事实上ASP连编程语言都不算,ASP只是提供了几个对象的接口给脚本语言进行编程。ASP默认支持的脚本语言是VBScript,但从2006年开始流行WEB 2.0的概念后,JavaScript成了最热门的编程语言之一。也就是从那时开始,不断有人采用JavaScript作为ASP的编程语言。JavaScript的语法比较灵活,特别是高阶函数、闭包等特性,比VBScript中规中矩的编程风格好用太多了。但比较遗憾的是,JavaScript默认的类型当中没有用于处理二进制数据的类型,处理二进制类型数据的时候,需要使用一定的技巧。ASP虽然已经流行过那么多年,但是一直没有出现一套比较好用的Web开发框架,多数应用都是停留在一个请求对应一个页面的处理方式。我设计了一个简单的MVC框架,用于应对一些简单快捷的开发需求,同时对这个问题提供一点解决思路,算是对ASP知识的一点总结。

1 运行环境

打开IIS Express的调试ASP的相关选项,用IIS Express来运行调试ASP是比较方便的,功能与相应版本的IIS是类似的,只是少了管理界面,特别对于还在跑Win 2003的主机,IIS Express可以让PHP、JSP等多种技术同时共享80端口提供服务。 打开IIS Express的配置文件C:\Documents and Settings\$YourName$\My Documents\IISExpress\config\applicationHost.config 找到节点 <system.webServer>下的<asp>节点修改为如下代码:

<asp 

enableParentPaths="true"
bufferingOn="true"
errorsToNTLog="true"
appAllowDebugging="true"
appAllowClientDebug="true"
scriptErrorSentToBrowser="true">

<session allowSessionState="true" />
<cache diskTemplateCacheDirectory="%TEMP%\iisexpress\ASP Compiled Templates" />
<limits />
</asp>

2 目录结构

想要实现rest风格的URL,需要通过rewrite规则来实现,可以实现单文件入口,完全割掉.asp这个尾巴。框架的目录结构:

  目录 文件 备注
├── controller   控制器
│   ├── index.asp  
│   └── user.asp  
├── model   模型
│   ├── article.asp  
│   └── user.asp  
├── view   视图
│   ├── index.asp  
├── index.asp   框架入口
└── web.config   配置信息

通过配置rewrite规则,除去URL中有对应的目录和文件,其他请求都转发到框架入口文件index.asp中。一次请求过程如下:首先检查url是否复合reset风格,读取相应的Controller和Action方法,读取过的Controller就会缓存到Application中,接着根据Action方法参数名,注入相应的请求参数,Action方法进行必要的数据验证之后,调用对应Model的方法取得数据,读取过的Model方法同样会缓存起来,最后把数据填充到对应的View中,同样View的预编译结果也会缓存起来。之后的请求就直接从内存中读取对应的脚本,不需要再通过adobe.stream组件读取脚本文件,请求的响应就会更加快。

<?xml version="1.0" encoding="UTF-8"?>

<configuration>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="314572800" />
</requestFiltering>
</security>
<defaultDocument>
<files>
<clear />
<add value="index.asp" />
</files>
</defaultDocument>

<rewrite>
<rules>
<rule name="Imported Rule 1" stopProcessing="true">
<match url="^(.*)$" ignoreCase="false"/>
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true"/>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true"/>
</conditions>
<action type="Rewrite" url="index.asp?url={R:1}" appendQueryString="true"/>
</rule>
</rules>
</rewrite>
</system.webServer>
<system.web>
<compilation debug="true" />
</system.web>
</configuration>

3 缓存

为了较少磁盘读写的次数,我将所有需要读入的文件都缓存到Application对象当中,比如模板、控制器、模型等。框架当中有三个函数用到了cache缓存函数,需要缓存的内容都通过cache回调函数来实现的。巧妙的地方在于cache函数的第二个参数是回调函数,把回调函数返回的内容进行缓存。比如readAllText函数是调用cache函数得到的返回值,cache函数的第一个参数是函数名+相对路径(为了区分不同函数读取同一个文件的情况),第二个参数传入一个封装了读写步骤的匿名函数。还有类似的缓存例子,就是模板引擎的实现,模板引擎先将模板读取进来,然后预编译成函数体,接着编译成函数,最后根据填充数据生成输入内容。因为Application对象只能缓存字符串,无法缓存函数对象,为了进一步减少运算成本,把函数体缓存到Application对象当中。

function readAllText(path,encoding){

return cache("readAllText:" + path,function(){
encoding = encoding || "utf-8";
path = Server.MapPath(path);
var fso = Server.CreateObject("Scripting.FileSystemObject");
if(!fso.FileExists(path)){
throw new Error(path + " is not exists!");
}
var stream = Server.CreateObject("adodb.stream");
stream.Mode = 3;
stream.Type = 2;
stream.Open();
stream.Charset = encoding;
stream.LoadFromFile(path);
var content = stream.readText();
stream.Close();
stream = null;
return content;
});

}
//缓存内容到Application
function cache(id,callback){
//方便调试
var debug = true;

if(debug){
Application.Lock();
Application.Contents.RemoveAll();
Application.UnLock();
}
if(Application(id)){
content = String(Application(id));
}else{
content = callback();
Application(id) = content;
}

return content;
}

4 包管理问题

引入类似Node.js包管理机制,只是一个简单的实现,通过函数导入模块而不是包含语句,可以简化代码结构,而所有的读文件的操作,都进行了缓存。这里涉及到3个函数

//导入模块

//path 相对于根目录的路径
//因为是单文件入口,路径的计算都是从根目录开始
function require(path){
var content = cache("require:" + path,function(){
var str = readAllText(path);
//去掉<script></script>标签
return str.replace(/(\s*)\u003cscript(.+?)\u003e/i,"")
.replace(/\u003c\/script\u003e(\s*)$/i,"")
//去掉条件编译选项
.replace(/\u003c%\s*@.*%\u003e/g,"")
//asp脚本的开始结束符号
.replace(/\u003c%/g,"")
.replace(/%\u003e/g, "") + "return exports";

});

//new Function 函数体内的没有定义的变量exports,会自动注册为全局变量
return new Function(content)();
}

5 模版引擎

说得好听叫模版引擎,其实就是一个函数,函数的原型来源于JQuery的作者John Resig的Micro-Templating,针对ASP的运行环境进行了一些调整。支持子模板,用<%=变量名%>形式输出填充数据。

//模板编译函数

function compile(path,data){
var data = data || {};
var fn_body = cache("compile:" + path,function(){
var str = readAllText(path);
return "var p=[];"
+ "p.push('"
+str
.replace(/<!--#include file=["'](.*)['"]-->/g,function(m,filePath){
filePath = path.substring(0,path.lastIndexOf("/") + 1) + filePath;
return compile(filePath);
})
.replace(/<!--#include virtual=["'](.*)['"]-->/g,function(m,filePath){
return compile(filePath);
})
.replace(/\u003c%\s*@.*%\u003e/g,"")
//删除单行注释
.replace(/\/\/[^\n]*/g,"")
.replace(/[\r\t\n]/g, " ")
.split('\\').join("\\\\")
.split("\u003c%").join("\t")
.replace(/((^|%\u003e)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%\u003e/g, "',$1,'")
.split("\t").join("');")
.split("%\u003e").join("p.push('")
.split("\r").join("\\'")
+ "');return p.join('');"
});

var fn = function(d) {
var p, k = [], v = [];
for (p in d) {
k.push(p);
v.push(d[p]);
};
return (new Function(k, fn_body)).apply(d, v);
};
return fn(data);
}
//输出模板内容
function render(path,data){
Response.Write(compile(path,data));
}

6 数据库操作

JavaScript比较强大的地方就在于可以将函数作为参数传递给另外一个函数。这一特性对于抽取共性的操作封装成函数或者库来说,就像是一把屠龙刀。下面就让我们见识一些它的威力。

//数据库查询函数

function query(sql,params,callback){
params = params || null;
var result = [];
var cmd = Server.CreateObject("Adodb.Command");
cmd.ActiveConnection = CONN_STRING;
cmd.CommandText = sql;
cmd.CommandType = 1; //adCmdText 1 adCmdStoredProc 4

if(callback){
result = callback(params,cmd);
}else{
var rs = cmd.execute(null,params);
if(rs.State == 1){
while(!rs.EOF){
var obj = {};
for(var i = 0; i < rs.Fields.Count; i++){
var name = rs.Fields(i).Name;
var value = rs.Fields(i).Value;
obj[name] = value;
}
result.push(obj);
rs.MoveNext();
}
rs.close();
}
rs = null;
}
cmd = null;
return result;
}

一些简单的数据库操作可以直接调用query函数,把SQL执行生成的表格数据转化为JavaScript对象数组,以便于将数据传递到视图。一些更复杂的情景下,比如调用存储过程,你需要设置参数名称,参数类型和参数长度才能正确调用,这时候你可以传入一个回调函数,用于生成数据模型对象,用于填充试图。

7 上传问题

JavaScript自带的函数库当中并没有用于处理二进制数据的方法,网络上的各路大神,想尽了各种方法,试图为JavaScript增加这个能力。有人通过MSScriptControl.ScriptControl控件,调用VBScript的函数来实现,有人通过Adodb.Steam对象,把二进制数据转换成unicode字符来处理,还有人将VBScript的上传类封装成sct组件。封装成sct组件的方式在代码执行效率和代码组织都是比较有优势的。曾经我也比较喜欢这样的解决方案,直到我见到第四种解决思路,我才豁然开朗。原来Microsoft.XMLDOM组件是具有处理二进制数据的能力的,通过指定节点的dataType为bin.hex,你就可以直接给节点的noteTypeValue赋值二进制数据,节点的text属性就是二进制数据对应的16进制字符串。对二进制的数据的分析就可以转换到对16进制字符串的分析。

function Upload(){

this.MAXSIZE = 30 * 1024 * 1024;
this.filter = "jpg|png|gif|bmp|doc|docx|txt|zip|rar";
var data = {}; // 存储表单对象
this.parseForm = function() {
var totalBytes = Request.TotalBytes;
if(totalBytes > this.MAXSIZE) return "文件大小超过限制。";
var boundary = Request.ServerVariables("Http_Content_Type").Item || "";
boundary = (boundary.match(/boundary=(.+)/) || [])[1];
if(!boundary) return "非文件上传表单";
//将boundary转换为用16进制表示的字符串
boundary = "2d2d" + boundary.replace(/[\w\-]/g, function(m){ return m.charCodeAt(0).toString(16); });

var xml = Server.CreateObject("Microsoft.XMLDOM");
var root = xml.createElement("root");
//指定节点的数据类型为bin.hex
root.dataType="bin.hex";
//创建一个recordset对象
var rs = Server.CreateObject("adodb.recordset");
//增加一个二进制类型的字段
rs.fields.Append("bin",205,-1);
rs.Open();
rs.AddNew();
var readBytes = 0; bufferSize = 200000;
//分块读入请求数据
while(Response.IsClientConnected() && readBytes < totalBytes){
rs(0).appendChunk(Request.BinaryRead(bufferSize));
readBytes += bufferSize;
}
root.nodeTypedValue = rs(0).value; rs.close();
//用boundary分割数据,去头,去尾
var multipart = root.text.split(boundary);multipart.shift(); multipart.pop();
var re = new RegExp("\\.(?:" + this.filter + ")$", "i");
for(var i = 0; i < multipart.length; i++){
//找到两个回车换行符号的位置
var indexOfDoubleCRLF = multipart[i].indexOf("0d0a0d0a");
//去掉开头的回车换行符号,然后用%分隔每个字节
var header = multipart[i].slice(4, indexOfDoubleCRLF).replace(/(\w{2})/g, "%$1");
header = decodeURIComponent(header);
var filename = (header.match(/filename="(.+?)"/i) || [])[1] || "";
filename = (filename.match(/([^\\\/]+)$/) || [])[1];
if(filename && !re.test(filename)) return "不允许的文件类型。";
// 查找两个空行,忽略最后一个空行
root.text = multipart[i].slice(indexOfDoubleCRLF + 8, -4);
var fieldName = (header.match(/name="(.+?)"/i) || [])[1] || "";
if(!data[fieldName]) data[fieldName] = [];
data[fieldName].push(
{
filename : filename,
size : root.text.length / 2,
value : filename ? root.nodeTypedValue : decodeURIComponent(root.text.replace(/(\w{2})/g, "%$1")),
saveAs:function(filepath){
var stream = Server.CreateObject("adodb.stream");
stream.type = 1;
stream.open();
stream.write(this.value);
stream.savetofile(filepath);
stream.close();
},
toString: function(){
return this.value;
}
});
}

return "OK";
}
this.getField = function(fieldName)
{

var item = data[fieldName] || [];
return item.length < 2 ? item[0] : item;
}
}
exports = new Upload();
Last Updated 2018-10-14 日 23:38.
Created by Emacs 25.1.1 (Org mode 9.1.14)